game_manager_lib\services\recommendation/
core.rs1use crate::models::{Game, GameTag};
7use crate::utils::tag_utils::TagKey;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11pub const MAX_TAG_CONTRIBUTION: f32 = 300.0; pub const WEIGHT_GENRE: f32 = 3.0;
15pub const WEIGHT_PLAYTIME_HOUR: f32 = 1.2;
16pub const WEIGHT_FAVORITE: f32 = 30.0;
17pub const WEIGHT_USER_RATING: f32 = 8.0;
18pub const WEIGHT_SERIES: f32 = 1.0;
19
20#[derive(Debug, Deserialize, Serialize, Clone)]
23pub struct RecommendationConfig {
24 pub content_weight: f32,
25 pub collaborative_weight: f32,
26 pub age_decay: f32,
27 pub favor_series: bool,
28}
29
30impl Default for RecommendationConfig {
31 fn default() -> Self {
32 Self {
33 content_weight: 0.65,
34 collaborative_weight: 0.35,
35 age_decay: 0.98,
36 favor_series: true,
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
42pub struct UserSettings {
43 pub filter_adult_content: bool,
44 pub series_limit: SeriesLimit,
45}
46
47impl Default for UserSettings {
48 fn default() -> Self {
49 Self {
50 filter_adult_content: false,
51 series_limit: SeriesLimit::Moderate,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy)]
57pub enum SeriesLimit {
58 None,
59 Moderate, Aggressive, }
62
63#[derive(Debug, Serialize, Clone)]
64pub struct RecommendationReason {
65 pub label: String,
66 pub type_id: String,
67}
68
69#[derive(Debug, Clone)]
70pub struct GameWithDetails {
71 pub game: Game,
72 pub genres: Vec<String>,
73 pub tags: Vec<GameTag>,
74 pub series: Option<String>,
75 pub release_year: Option<i32>,
76 pub steam_app_id: Option<u32>,
77}
78
79#[derive(Debug, Serialize, Deserialize, Clone)]
80pub struct UserPreferenceVector {
81 pub genres: HashMap<String, f32>,
82 #[serde(
83 serialize_with = "serialize_tags",
84 deserialize_with = "deserialize_tags"
85 )]
86 pub tags: HashMap<TagKey, f32>,
87 pub series: HashMap<String, f32>,
88 #[serde(rename = "totalPlaytime")]
89 pub total_playtime: i32,
90 #[serde(rename = "totalGames")]
91 pub total_games: i32,
92}
93
94fn serialize_tags<S>(tags: &HashMap<TagKey, f32>, serializer: S) -> Result<S::Ok, S::Error>
96where
97 S: serde::Serializer,
98{
99 use serde::ser::SerializeMap;
100 let mut map = serializer.serialize_map(Some(tags.len()))?;
101 for (key, value) in tags {
102 let key_string = format!("{:?}:{}", key.category, key.slug);
103 map.serialize_entry(&key_string, value)?;
104 }
105 map.end()
106}
107
108fn deserialize_tags<'de, D>(deserializer: D) -> Result<HashMap<TagKey, f32>, D::Error>
110where
111 D: serde::Deserializer<'de>,
112{
113 let map: HashMap<String, f32> = HashMap::deserialize(deserializer)?;
114 let mut result = HashMap::new();
115
116 for (key_string, value) in map {
117 let parts: Vec<&str> = key_string.split(':').collect();
118 if parts.len() == 2 {
119 if let Ok(category) = serde_json::from_str(&format!("\"{}\"", parts[0])) {
120 let key = TagKey::new(category, parts[1].to_string());
121 result.insert(key, value);
122 }
123 }
124 }
125
126 Ok(result)
127}
128
129pub fn parse_release_year(date_str: &str) -> Option<i32> {
133 date_str.split('-').next()?.parse().ok()
134}
135
136pub fn calculate_game_weight(game: &Game) -> f32 {
138 let playtime_hours = (game.playtime.unwrap_or(0) / 60).min(100);
139 let mut weight = 1.0 + (playtime_hours as f32 * WEIGHT_PLAYTIME_HOUR);
140
141 if game.favorite {
142 weight += WEIGHT_FAVORITE;
143 }
144
145 if let Some(rating) = game.user_rating {
146 weight += (rating as f32) * WEIGHT_USER_RATING;
147 }
148
149 weight
150}